fix(display): close the video writer when the audio writer's close throws - #291
Merged
harsha509 merged 1 commit intoAug 11, 2026
Merged
Conversation
…rows
recordScreenAndAudioToFiles closed its two writers in sequence:
audioWritten = await audioOut.close();
await videoOut.close();
M4aFileWriter finalizes by appending the moov box, ending the stream, then
reopening the finished file to patch mdat's length field. Any of those can fail
on a full disk or a reaped temp directory, long after every frame is already
safely on disk. When one does, the video writer's close is skipped.
The cost is a leaked file descriptor. Without close() the write stream is never
ended, and Node does not release descriptors on garbage collection, so it lives
until the process exits. The error the stream is holding also goes unread, since
AnnexBFileWriter only surfaces that from write() or close().
The video file itself is intact. Node's write queue drains on its own once
chunks are handed to write(), the recording loop awaits every write, and Annex-B
is an elementary stream with no index or footer to finalize, so a file ending
after the last frame is valid and playable. Verified by writing 15 MB and never
calling close(): every byte was on disk.
Closing the video writer from a finally makes it independent of the audio
writer's failure. The audio error still propagates when the video close
succeeds. If both throw the video error supersedes, which is standard finally
behaviour and an acceptable edge for a path that has already failed.
Covered by a unit test that stubs the audio writer's close to throw and asserts
the video writer was still closed. Reverting to sequential closes fails it with
0 closes instead of 1.
mykola-mokhnach
approved these changes
Aug 11, 2026
harsha509
deleted the
fix/display-close-video-writer-when-audio-close-fails
branch
August 11, 2026 19:42
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 11, 2026
## [5.14.3](v5.14.2...v5.14.3) (2026-08-11) ### Bug Fixes * **display:** close the video writer when the audio writer's close throws ([#291](#291)) ([ee8f381](ee8f381))
|
🎉 This PR is included in version 5.14.3 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The issue
recordScreenAndAudioToFilesclosed its two writers in sequence:If the audio close throws, the video close never runs.
That is not a hypothetical ordering nit.
M4aFileWriter.finalize()has four throw sites, all reached after the recording has succeeded:The last pair is the notable one: the writer reopens the finished
.m4ato patchmdat's length. A disk that fills during a long recording, or a temp directory reaped underneath it, fails there once every frame is already safely on disk.What it actually costs
A leaked file descriptor. Without
close(),stream.end()is never called, and Node does not release descriptors on garbage collection, so it lives until the process exits.A silently held error.
AnnexBFileWriterkeeps the first stream error and surfaces it fromwrite()orclose(). Withclose()skipped, nobody ever reads it.That second part got quieter as a side effect of #289. Before
AnnexBFileWriterexisted, an abandoned stream that errored later produced an uncaught'error'and was at least loud. Now the permanent listener holds it and no one looks. Noisy to silent.What it does not cost
Worth stating plainly, because an earlier draft of this analysis got it wrong: the video file is intact. Node's write queue drains autonomously once chunks are handed to
write(), the recording loop awaits everyvideoOut.write(), and Annex-B is an elementary stream with no index or footer to finalize — unlike.m4a'smdatpatch. A file ending after the last written frame is valid and playable. Verified by writing 15 MB throughAnnexBFileWriterand never callingclose():The caller does not get a misleading result either: the function rejects, so no result object with
framesWrittenis ever returned.So: one leaked fd plus an unread error, on a path that has already failed. Genuinely minor — worth fixing because it is two lines, not because it is dangerous.
The fix
The audio error still propagates when the video close succeeds. If both throw, the video error supersedes via standard
finallybehaviour — an acceptable edge on a path that has already failed, and the same judgement reached in review of the earlier PRs.Tests
A unit test stubs
M4aFileWriter.close()to throw and countsAnnexBFileWriter.close()calls, using the repo's existingmockImporthelper. The mock merges over the real exports, soScreenStreamCapturestays intact and the capture path runs for real against a stubDisplayService.Mutation-checked by reverting to sequential closes in the compiled output:
Only the new test fails, with the intended assertion.
tsc --noEmitoxlintoxfmt --checkNot verified against hardware: this path needs an iOS 27 device (
npm run test:display). What is verified is the close ordering that was wrong.Scope
This is the third and last of the resource-lifetime findings from reviewing the DisplayService recording paths, after #289 (uncaught write-stream errors) and #290 (captures stranded when the output file cannot be created).
One unrelated finding remains open and is deliberately not included here:
parseRtpPacketignores the RTP padding bit (data[0] & 0x20), so padding octets would be handed downstream as payload. It is spec conformance with no evidence the device sets the bit, and it belongs in its own PR rather than inside one about file descriptors.